/*
    The MIT License (MIT)

    Copyright (c) 2015 Andreas Marek and Contributors

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
    (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
    publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
    so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.intellij.lang.jsgraphql.types.schema;


import com.intellij.lang.jsgraphql.types.DirectivesUtil;
import com.intellij.lang.jsgraphql.types.PublicApi;
import com.intellij.lang.jsgraphql.types.language.InputValueDefinition;
import com.intellij.lang.jsgraphql.types.util.TraversalControl;
import com.intellij.lang.jsgraphql.types.util.TraverserContext;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import static com.intellij.lang.jsgraphql.types.Assert.assertNotNull;
import static com.intellij.lang.jsgraphql.types.Assert.assertValidName;

/**
 * This defines an argument that can be supplied to a graphql field (via {@link com.intellij.lang.jsgraphql.types.schema.GraphQLFieldDefinition}.
 * <p>
 * Fields can be thought of as "functions" that take arguments and return a value.
 * <p>
 * See http://graphql.org/learn/queries/#arguments for more details on the concept.
 * <p>
 * {@link com.intellij.lang.jsgraphql.types.schema.GraphQLArgument} is used in two contexts, one context is graphql queries where it represents the arguments that can be
 * set on a field and the other is in Schema Definition Language (SDL) where it can be used to represent the argument value instances
 * that have been supplied on a {@link com.intellij.lang.jsgraphql.types.schema.GraphQLDirective}.
 * <p>
 * The difference is the 'value' and 'defaultValue' properties.  In a query argument, the 'value' is never in the GraphQLArgument
 * object but rather in the AST direct or in the query variables map and the 'defaultValue' represents a value to use if both of these are
 * not present. You can think of them like a descriptor of what shape an argument might have.
 * <p>
 * However with directives on SDL elements, the value is specified in AST only and transferred into the GraphQLArgument object and the
 * 'defaultValue' comes instead from the directive definition elsewhere in the SDL.  You can think of them as 'instances' of arguments, their shape and their
 * specific value on that directive.
 */
@PublicApi
public class GraphQLArgument implements GraphQLNamedSchemaElement, GraphQLInputValueDefinition {

  private final String name;
  private final String description;
  private final String deprecationReason;
  private final GraphQLInputType originalType;
  private final Object value;
  private final Object defaultValue;
  private final InputValueDefinition definition;
  private final DirectivesUtil.DirectivesHolder directives;

  private GraphQLInputType replacedType;

  public static final String CHILD_DIRECTIVES = "directives";
  public static final String CHILD_TYPE = "type";

  private static final Object DEFAULT_VALUE_SENTINEL = new Object() {
  };

  /**
   * @param name         the arg name
   * @param description  the arg description
   * @param type         the arg type
   * @param defaultValue the default value
   * @param definition   the AST definition
   * @deprecated use the {@link #newArgument()} builder pattern instead, as this constructor will be made private in a future version.
   */
  @Deprecated(forRemoval = true)
  public GraphQLArgument(String name, String description, GraphQLInputType type, Object defaultValue, InputValueDefinition definition) {
    this(name, description, type, defaultValue, null, definition, Collections.emptyList(), null);
  }

  private GraphQLArgument(String name,
                          String description,
                          GraphQLInputType type,
                          Object defaultValue,
                          Object value,
                          InputValueDefinition definition,
                          List<GraphQLDirective> directives,
                          String deprecationReason) {
    assertValidName(name);
    assertNotNull(type, () -> "type can't be null");
    this.name = name;
    this.description = description;
    this.deprecationReason = deprecationReason;
    this.originalType = type;
    this.defaultValue = defaultValue;
    this.value = value;
    this.definition = definition;
    this.directives = new DirectivesUtil.DirectivesHolder(directives);
  }


  void replaceType(GraphQLInputType type) {
    this.replacedType = type;
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public GraphQLInputType getType() {
    return replacedType != null ? replacedType : originalType;
  }

  /**
   * An argument has a default value when it represents the logical argument structure that a {@link com.intellij.lang.jsgraphql.types.schema.GraphQLFieldDefinition}
   * can have and it can also have a default value when used in a schema definition language (SDL) where the
   * default value comes via the directive definition.
   *
   * @return the default value of an argument
   */
  public Object getDefaultValue() {
    return defaultValue == DEFAULT_VALUE_SENTINEL ? null : defaultValue;
  }

  public boolean hasSetDefaultValue() {
    return defaultValue != DEFAULT_VALUE_SENTINEL;
  }

  /**
   * An argument ONLY has a value when its used in a schema definition language (SDL) context as the arguments to SDL directives.  The method
   * should not be called in a query context, but rather the AST / variables map should be used to obtain an arguments value.
   *
   * @return the argument value
   */
  public Object getValue() {
    return value;
  }

  @Override
  public String getDescription() {
    return description;
  }

  public String getDeprecationReason() {
    return deprecationReason;
  }

  public boolean isDeprecated() {
    return deprecationReason != null;
  }

  @Override
  public InputValueDefinition getDefinition() {
    return definition;
  }

  @Override
  public List<GraphQLDirective> getDirectives() {
    return directives.getDirectives();
  }

  @Override
  public Map<String, GraphQLDirective> getDirectivesByName() {
    return directives.getDirectivesByName();
  }

  @Override
  public Map<String, List<GraphQLDirective>> getAllDirectivesByName() {
    return directives.getAllDirectivesByName();
  }

  @Override
  public GraphQLDirective getDirective(String directiveName) {
    return directives.getDirective(directiveName);
  }

  @Override
  public List<GraphQLSchemaElement> getChildren() {
    List<GraphQLSchemaElement> children = new ArrayList<>();
    children.add(getType());
    children.addAll(directives.getDirectives());
    return children;
  }


  @Override
  public SchemaElementChildrenContainer getChildrenWithTypeReferences() {
    return SchemaElementChildrenContainer.newSchemaElementChildrenContainer()
      .children(CHILD_DIRECTIVES, directives.getDirectives())
      .child(CHILD_TYPE, originalType)
      .build();
  }

  @Override
  public GraphQLArgument withNewChildren(SchemaElementChildrenContainer newChildren) {
    return transform(builder ->
                       builder.type(newChildren.getChildOrNull(CHILD_TYPE))
                         .replaceDirectives(newChildren.getChildren(CHILD_DIRECTIVES)));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public final boolean equals(Object o) {
    return super.equals(o);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public final int hashCode() {
    return super.hashCode();
  }


  /**
   * This helps you transform the current GraphQLArgument into another one by starting a builder with all
   * the current values and allows you to transform it how you want.
   *
   * @param builderConsumer the consumer code that will be given a builder to transform
   * @return a new field based on calling build on that builder
   */
  public GraphQLArgument transform(Consumer<Builder> builderConsumer) {
    Builder builder = newArgument(this);
    builderConsumer.accept(builder);
    return builder.build();
  }

  public static Builder newArgument() {
    return new Builder();
  }

  public static Builder newArgument(GraphQLArgument existing) {
    return new Builder(existing);
  }

  @Override
  public TraversalControl accept(TraverserContext<GraphQLSchemaElement> context, GraphQLTypeVisitor visitor) {
    return visitor.visitGraphQLArgument(this, context);
  }

  @Override
  public String toString() {
    return "GraphQLArgument{" +
           "name='" + name + '\'' +
           ", value=" + value +
           ", defaultValue=" + defaultValue +
           ", type=" + getType() +
           '}';
  }

  public static class Builder extends GraphqlTypeBuilder {

    private GraphQLInputType type;
    private Object defaultValue = DEFAULT_VALUE_SENTINEL;
    private Object value;
    private String deprecationReason;
    private InputValueDefinition definition;
    private final List<GraphQLDirective> directives = new ArrayList<>();

    public Builder() {
    }

    public Builder(GraphQLArgument existing) {
      this.name = existing.getName();
      this.type = existing.originalType;
      this.value = existing.getValue();
      this.defaultValue = existing.defaultValue;
      this.description = existing.getDescription();
      this.deprecationReason = existing.getDeprecationReason();
      this.definition = existing.getDefinition();
      DirectivesUtil.enforceAddAll(this.directives, existing.getDirectives());
    }

    @Override
    public Builder name(String name) {
      super.name(name);
      return this;
    }

    @Override
    public Builder description(String description) {
      super.description(description);
      return this;
    }

    @Override
    public Builder comparatorRegistry(GraphqlTypeComparatorRegistry comparatorRegistry) {
      super.comparatorRegistry(comparatorRegistry);
      return this;
    }

    public Builder definition(InputValueDefinition definition) {
      this.definition = definition;
      return this;
    }

    public Builder deprecate(String deprecationReason) {
      this.deprecationReason = deprecationReason;
      return this;
    }

    public Builder type(GraphQLInputType type) {
      this.type = type;
      return this;
    }

    public Builder defaultValue(Object defaultValue) {
      this.defaultValue = defaultValue;
      return this;
    }

    public Builder value(Object value) {
      this.value = value;
      return this;
    }

    public Builder withDirectives(GraphQLDirective... directives) {
      assertNotNull(directives, () -> "directives can't be null");
      this.directives.clear();
      for (GraphQLDirective directive : directives) {
        withDirective(directive);
      }
      return this;
    }

    public Builder withDirective(GraphQLDirective directive) {
      assertNotNull(directive, () -> "directive can't be null");
      DirectivesUtil.enforceAdd(this.directives, directive);
      return this;
    }

    public Builder replaceDirectives(List<GraphQLDirective> directives) {
      assertNotNull(directives, () -> "directive can't be null");
      this.directives.clear();
      DirectivesUtil.enforceAddAll(this.directives, directives);
      return this;
    }

    public Builder withDirective(GraphQLDirective.Builder builder) {
      return withDirective(builder.build());
    }

    /**
     * This is used to clear all the directives in the builder so far.
     *
     * @return the builder
     */
    public Builder clearDirectives() {
      directives.clear();
      return this;
    }


    public GraphQLArgument build() {
      return new GraphQLArgument(
        name,
        description,
        type,
        defaultValue,
        value,
        definition,
        sort(directives, GraphQLArgument.class, GraphQLDirective.class),
        deprecationReason
      );
    }
  }
}
